package org.cogaen.net;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.util.HashMap;
import java.util.Map;

import org.cogaen.core.Core;
import org.cogaen.core.UpdateableService;
import org.cogaen.logging.LoggingService;

public class NetworkService implements UpdateableService {

	/** Unique name of this service. */
	public static final String NAME = "cogaen.networkservice";
	
	/** Identifier used for logging messages. */
	public static final String LOGGING_SOURCE = "Network";

	/** Used to send all datagram packages. */
	private DatagramChannel channel;
	
	/** Used to receive messages via UDP. */
	private ByteBuffer buffer = ByteBuffer.allocate(1024);
	
	/** All peers created by this service. */
	private Map<SocketAddress, PeerImpl> peers = new HashMap<SocketAddress, PeerImpl>();
			
	/** Reference to service locator. */
	private Core core;
	
	/**
	 * Returns the installed instance of this service.
	 * 
	 * <p>This is a convenience method. A call to this method is identical
	 * to the following code:
	 * <pre>
	 * (NetworkService) core.getService( NetworkService.NAME );
	 * </pre>
	 * </p>
	 * 
	 * @param core reference to a instance of core.
	 * @return installed service instance.
	 */
	public static NetworkService getInstance(Core core) {
		return (NetworkService) core.getService(NAME);
	}
	
	/**
	 * Creates a new instance of this service.
	 * This service is bound it to any available port on the local host machine. 
	 * This constructor is typically used for client code. 
	 * 
	 * @throws IOException if an I/O error occurs
	 */
	public NetworkService() throws IOException {
		this.channel = DatagramChannel.open();
		this.channel.configureBlocking(false);
		
		int bufferSize = this.channel.socket().getReceiveBufferSize();
		this.buffer = ByteBuffer.allocate( bufferSize );
	}
	
	public void freeResources() {
		this.channel.socket().close();
		try {
			this.channel.close();
		} catch (IOException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	/**
	 * Creates a new instance of this service.
	 * This service is bound to the specified port.
	 * This constructor is typically used for server code. 
	 * 
	 * @param port integer specifying the port this service is bound to.
	 * @throws IOException if an I/O error occurs
	 */
	public NetworkService(int port) throws IOException {
		this();
		this.channel.socket().bind( new InetSocketAddress(port) );		
	}

	/**
	 * Creates a new peer object bound to the specified address. 
	 * The returned peer object will not be connected.
	 * 
	 * @param hostname the host name
	 * @param port the port number
	 * @return the newly created peer object.
	 * 
	 * @see connect
	 */
	public Peer createPeer(String hostname, int port) {
		return createPeer( new InetSocketAddress(hostname, port) );
	}
		
	public void destroyPeer(Peer peer) {
		this.peers.remove(peer);
	}
	
	private PeerImpl createPeer(SocketAddress address) {
		PeerImpl peer = new PeerImpl( this.core, address );
		this.peers.put( address, peer );
		
		return peer;
	}
	
	private void receiveData() throws IOException {
		this.buffer.clear();
		SocketAddress address = this.channel.receive( this.buffer );
		
		while ( address != null ) {
			PeerImpl peer = this.peers.get( address );
			if (peer == null) {
				peer = createPeer( address );
			}
			
			this.buffer.flip();
			peer.putPackage( this.buffer );
			
			this.buffer.clear();
			address = this.channel.receive( this.buffer );
		}
	}
		
	@Override
	public void update() {
		try {
			receiveData();
			
			for ( PeerImpl peer: this.peers.values() ) {
				peer.update( this.channel );
			}
			
		} catch (IOException e) {
			LoggingService.getInstance( this.core ).logError(LOGGING_SOURCE, e.getMessage() );
		}
	}

	@Override
	public String getName() {
		return NAME;
	}

	@Override
	public void initialize(Core core) {
		this.core = core;
	}
}
